# -*- coding: utf-8 -*-
"""
@Time : 2024/1/24 16:37
@Author : TJF
"""
import re
from time import sleep

from sweetest.log import logger
from sweetest.globals import g, now, timestamp
from sweetest.elements import e
from sweetest.windows_tools import w
from sweetest.snapshot import Snapshot
from sweetest.keywords import mobile, files, common, windows, web, http
from sweetest.config import (
    web_keywords, mobile_keywords, http_keywords, files_keywords,
    windows_keywords, windows_alias, common_keywords
)
from sweetest.utility import replace_dict, replace


# ----------------------------- 工具函数 -----------------------------

def elements_format(page, element):
    # 如果未指定页面，则使用当前页面
    if not page:
        page = g.current_page

    # 如果未指定元素，则返回当前页面和空的自定义和元素
    if not element:
        return page, '', element

    # 如果页面为SNIPPET或用例片段，或元素为变量赋值，则返回当前页面和空的自定义和元素
    if page in ('SNIPPET', '用例片段') or element in ('变量赋值',):
        return page, '', element

    # 如果元素包含多个部分，按|分割
    elements = element.split('|')
    if len(elements) == 1:
        # 如果只有一个元素部分，调用e.have获取自定义和元素
        custom, el = e.have(page, element)
        # 更新当前页面
        g.current_page = page
        return page, custom, el
    else:
        # 如果有多个元素部分，逐个获取自定义和元素
        els = []
        for _element in elements:
            custom, el = e.have(page, _element.strip())
            els.append(el)
        # 更新当前页面
        g.current_page = page
        return page, custom, els

def v_data(d, _d):
    data = ''
    # 根据是否包含',,'选择分隔符
    if ',,' in str(_d):
        s = ',,'
    else:
        s = ','

    # 将字典中的键值对以k=v形式拼接，并用分隔符隔开
    for k, v in d.items():
        data += k + '=' + str(v) + s

    # 根据分隔符类型去掉末尾多余的分隔符
    if s == ',,':
        return data[:-2]
    else:
        return data[:-1]

def test_v_data():
    # 测试用例数据
    data = {'a': 1, 'b': 'b'}
    _data = "{'a': 1,, 'b': 'b'}"
    # 调用v_data函数
    return v_data(data, _data)


# def v_data(d, _d):
#     """与原框架保持一致的 v_data 串化（按 ',' 拼接）。"""
#     data = ''
#     s = ','
#     for k, v in d.items():
#         data += k + '=' + str(v) + s
#     return data[:-1] if data else ''


def _parse_kv(s: str, allow_comma: bool = True) -> dict:
    """
    解析 K=V 文本为 dict。
    - allow_comma=True: 以逗号/分号/特定空格分隔（用于【测试数据】）
    - allow_comma=False: 不用逗号分隔，避免把 XPath/CSS 里的逗号切坏（用于【元素】）
    """
    if not s or not isinstance(s, str):
        return {}
    s = s.strip()
    if allow_comma:
        splitter = r'[;,]\s*|\s{2,}|\s(?=[^=]*=)'
    else:
        splitter = r';\s*|\s{2,}|\s(?=[^=]*=)'  # 不按逗号切
    parts = re.split(splitter, s)
    out = {}
    for p in parts:
        if not p or '=' not in p:
            continue
        k, v = p.split('=', 1)
        out[k.strip()] = v.strip()
    return out


def _parse_kv_for_element(element_text: str) -> dict:
    """仅给【元素】列使用，禁止逗号切割，避免破坏 XPath/CSS。"""
    return _parse_kv(element_text, allow_comma=False)


def _normalize_selector(element_text: str) -> str:
    """
    仅从【元素】列解析选择器：
    - 保留第一段；若有分号，自动截断到分号之前
    - 支持 'xpath=...' / 'css=...' / 裸 XPath（以 // 或 ( 开头）
    """
    s = (element_text or '').strip()
    if not s:
        return ''
    if ';' in s:
        head = s.split(';', 1)[0].strip()
        if head != s:
            logger.info(f"# 规范化元素：忽略分号后的内容：{s!r} -> {head!r}")
        s = head
    low = s.lower()
    if low.startswith('xpath=') or low.startswith('css='):
        return s
    if s.startswith('//') or s.startswith('('):
        return f'xpath={s}'
    return s



# ----------------------------- 主执行类 -----------------------------

class TestCase:
    def __init__(self, testcase):
        self.testcase = testcase
        self.snippet_steps = {}

    def run(self):
        self.testcase['title'] = replace(self.testcase['title'])
        logger.info('>>> 运行测试用例：%s|%s' % (self.testcase['id'], self.testcase['title']))
        logger.info('-' * 50)
        self.testcase['result'] = 'success'
        self.testcase['report'] = ''
        if_result = ''

        for index, step in enumerate(self.testcase['steps']):
            step['start_timestamp'] = timestamp()
            step['snapshot'] = {}

            # then / else 控制流（保持原逻辑）
            if step['control'] == '>' and not if_result:
                step['score'] = '-'
                step['end_timestamp'] = timestamp()
                logger.info('跳过 <then> 步骤：%s|%s|%s|%s' %
                            (step['no'], step['page'], step['keyword'], step['element']))
                continue
            if step['control'] == '<' and if_result:
                step['score'] = '-'
                step['end_timestamp'] = timestamp()
                logger.info('跳过 <else> 步骤：%s|%s|%s|%s' %
                            (step['no'], step['page'], step['keyword'], step['element']))
                continue

            # Windows/Electron 的步骤不走元素库格式化，避免把 xpath 等当成“页面元素”去查表
            if not (g.platform.lower() in ('windows',) and step['keyword'].upper() in windows_keywords):
                step['page'], step['custom'], step['element'] = elements_format(step['page'], step['element'])

            label = g.sheet_name + '#' + self.testcase['id'] + '#' + \
                str(step['no']).replace('<', '(').replace('>', ')').replace('*', 'x')

            logger.info('运行步骤：%s|%s|%s|%s' %
                        (step['no'], step['page'], step['keyword'], step['element']))

            snap = Snapshot()

            try:
                after_function = step['data'].pop('AFTER_FUNCTION', '')

                # 强制等待 / 全局缓步（保持原逻辑）
                if '#wait_time' in step['data']:
                    t = step['data'].pop('#wait_time', 0)
                    sleep(float(t))
                else:
                    if '#wait_times' in step['data']:
                        g.wait_times = float(step['data'].pop('#wait_times', 0))
                    if g.wait_times:
                        sleep(g.wait_times)

                # 变量替换（保持原逻辑）
                if step['page']:
                    step['page'] = replace(step['page'])

                if isinstance(step['element'], str):
                    step['element'] = replace(step['element'])
                    step['_element'] = step['element']
                elif isinstance(step['element'], list):
                    for i in range(len(step['element'])):
                        step['element'][i] = replace(step['element'][i])
                    step['_element'] = '|'.join(step['element'])

                replace_dict(step['data'])
                replace_dict(step['expected'])
                step['data'].pop('BEFORE_FUNCTION', '')
                step['vdata'] = v_data(step['data'], step['_data'])

                # ----------------- Web（保持原有逻辑） -----------------
                if g.platform.lower() in ('desktop',) and step['keyword'].upper() in web_keywords:
                    snap.pre(step, label)
                    if step['keyword'].upper() not in ('MESSAGE', '对话框'):
                        w.switch_window(step['page'])
                        frame = replace(step['custom'])
                        w.switch_frame(frame)
                    element = getattr(web, step['keyword'].lower())(step)
                    snap.web_shot(step, element)

                # ----------------- Mobile（保持原有逻辑） -----------------
                elif g.platform.lower() in ('ios', 'android') and step['keyword'].upper() in mobile_keywords:
                    context = replace(step['custom']).strip()
                    w.switch_context(context)
                    if w.current_context.startswith('WEBVIEW'):
                        tab = step['data'].get('标签页')
                        if tab:
                            del step['data']['标签页']
                            g.driver.switch_to_window(w.windows[tab])
                        logger.info('Current Context: %s' % repr(w.current_context))
                    getattr(mobile, step['keyword'].lower())(step)

                # ----------------- 旧 Windows（保持原逻辑） -----------------
                elif g.platform.lower() in ('window1sold',) and step['keyword'].upper() in windows_keywords:
                    _page = ''
                    if step['page'].startswith('#'):
                        _page = step['page'][1:]
                        page = [x for x in re.split(r'(<|>)', _page) if x != '']
                    else:
                        page = [x for x in re.split(r'(<|>)', step['page']) if x != '']
                    if _page:
                        dialog = g.windows['#'].dialog(page)
                    else:
                        dialog = g.windows['default'].dialog(page)
                    snap.pre(step, label)
                    window_instance = g.windows.get('default')
                    if window_instance:
                        getattr(window_instance, step['keyword'].lower())(step)
                    else:
                        raise RuntimeError("Windows实例未正确初始化")
                    snap.windows_shot(dialog, step)

                # ----------------- 新 Windows / Electron（JSONL worker） -----------------
                elif g.platform.lower() in ('windows',) and step['keyword'].upper() in windows_keywords:
                    keyword = windows_keywords[step['keyword'].upper()]  # 归一化后的动词
                    selector = step['element']
                    # Excel 三列容错：优先 '元素'（selector 或 k=v），再看 '测试数据'
                    element_text = (step.get('element') or step.get('元素') or '').strip()
                    data_text    = (step.get('testdata') or step.get('测试数据') or '').strip()
                    testdata_text = data_text  # ✅ 修正：测试数据来自【测试数据】列，而不是【元素】列

                    # 解析 K=V；元素列不按逗号切，测试数据列允许逗号/分号
                    kv_elem = _parse_kv_for_element(element_text)
                    kv_data = _parse_kv(data_text, allow_comma=True)
                    kv = {**kv_elem, **kv_data}
                    # ✨ 新增：把 kv 每个 value 做一次变量/表达式替换
                    kv = {k: (replace(v) if isinstance(v, str) else v) for k, v in kv.items()}

                    # 常见参数兜底
                    timeout_ms = int(kv.get('timeout') or kv.get('wait') or 5000)
                    wait_ms    = int(kv.get('waitMs') or kv.get('wait') or 5000)

                    if keyword == 'WAIT_FOR':
                        selector = _normalize_selector(element_text) or kv.get('selector') or kv.get('xpath') or kv.get('css')
                        if not selector:
                            raise RuntimeError("WAIT_FOR 需要 selector/xpath/css 或在【元素】列提供选择器")
                        logger.info(f"# [WAIT_FOR] selector={selector} timeout={timeout_ms}ms")
                        g.driver.wait_for(selector, timeout_ms=timeout_ms)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'CLICK':
                        # selector = _normalize_selector(element_text) or kv.get('selector') or kv.get('xpath') or kv.get('css')
                        # if not selector:
                        #     raise RuntimeError("CLICK 需要 selector/xpath/css 或在【元素】列提供选择器")
                        # g.driver.wait_for(selector, timeout_ms=timeout_ms)
                        logger.info(f"# [CLICK] selector={selector}")
                        g.driver.click(selector, timeout_ms=timeout_ms)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'INPUT':
                        # 固定“清空并填充”，输入内容只从【测试数据】取 text=...
                        selector = _normalize_selector(element_text) or kv.get('selector') or kv.get('xpath') or kv.get('css')
                        if not selector:
                            raise RuntimeError("INPUT 需要 selector/xpath/css 或在【元素】列提供选择器")
                        text = step['data']['text']
                        if text is None:
                            raise RuntimeError("INPUT 需要在【测试数据】里提供 text=...")
                        if timeout_ms:
                            g.driver.wait_for(selector, timeout_ms=timeout_ms)
                        logger.info(f"# [INPUT] selector={selector} text={text!r}")
                        g.driver.input(selector=selector, text=text, timeout=timeout_ms, delay=int(kv.get('delay', 0)))
                        step['result'] = 'success'
                        continue

                    elif keyword == 'CHECK':
                        # 允许在【元素】列直接写裸 XPath：//... 或带前缀 xpath=...
                        selector = kv.get('selector') or kv.get('xpath') or kv.get('css') or _normalize_selector(
                            element_text)
                        if not selector:
                            raise RuntimeError("CHECK 需要 selector/xpath/css 或在【元素】列提供选择器")

                        # 可选：从测试数据里覆盖超时
                        timeout_ms = int(kv.get('timeout') or kv.get('wait') or timeout_ms or 5000)

                        logger.info(f"# [CHECK] selector={selector} timeout={timeout_ms}ms")

                        # 直接复用 wait_for（worker 端默认 state='visible'）
                        g.driver.wait_for(selector, timeout_ms=timeout_ms)

                        step['result'] = 'success'
                        continue

                    elif keyword == 'TYPE':
                        # 若提供 selector，则对该元素输入；否则对当前焦点直接输入
                        text = kv.get('text')
                        selector = _normalize_selector(element_text) or kv.get('selector') or kv.get('xpath') or kv.get('css')
                        delay = int(kv.get('delay') or 10)
                        if selector:
                            if text is None:
                                raise RuntimeError("TYPE 需要在【测试数据】里提供 text=... 或改用 INPUT")
                            logger.info(f"# [TYPE] selector={selector} text={text!r} delay={delay}ms")
                            g.driver.type(selector=selector, text=text, replace=bool(kv.get('replace', False)),
                                          timeout=timeout_ms, delay=delay)
                        else:
                            if text is None:
                                raise RuntimeError("TYPE 需要在【测试数据】里提供 text=...，或提供元素选择器")
                            logger.info(f"# [TYPE] (active) text={text!r} delay={delay}ms")
                            # 对当前焦点输入，复用 send_keys
                            g.driver.send_keys(text)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'PRESS':
                        # from kv / 测试数据里取按键；支持两种写法：
                        #   1) 测试数据：keys=Enter 或 keys=Control+S;times=2
                        #   2) 直接把键名写在“测试数据”整格里：Enter / Alt+F4
                        keys = kv.get('keys') or testdata_text or step['data'].get('keys', '')
                        if not keys:
                            raise RuntimeError("PRESS 需要提供按键：例如 keys=Enter 或在【测试数据】写 Enter/Alt+F4")

                        times = int(kv.get('times') or 1)

                        # 选择器可选；给了就先聚焦（worker 会处理），没给就对当前页面执行快捷键
                        raw_selector = step['element'] or kv.get('selector') or kv.get('xpath') or kv.get('css') or ''
                        selector = _normalize_selector(raw_selector) if raw_selector else ""

                        timeout_ms = int(kv.get('timeout') or kv.get('wait') or 5000)

                        logger.info(f"# [PRESS] keys={keys} times={times} selector={selector or '(active)'}")
                        g.driver.press(keys=keys, selector=selector, times=times, timeout_ms=timeout_ms)

                        step['result'] = 'success'
                        continue


                    elif keyword == 'SEND_KEYS':
                        # 直接把文本打给当前活动窗口（不定位元素）
                        if not kv.get('text'):
                            raise RuntimeError("SEND_KEYS 需要在【测试数据】里提供 text=...")
                        g.driver.send_keys(kv['text'])
                        step['result'] = 'success'
                        continue

                    elif keyword == 'SLEEP':
                        ms = int(testdata_text or kv.get('wait') or 500)
                        g.driver.sleep(ms)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'SET_CLIPBOARD':
                        if not testdata_text:
                            raise RuntimeError("SET_CLIPBOARD 需要在【测试数据】里提供要写入剪贴板的文本")
                        g.driver.set_clipboard(testdata_text)
                        step['result'] = 'success'
                        continue

                    # ------ testcase.py 的 Windows/Electron 分支里，找到 STUB_SAVE_DIALOG，整段替换 ------
                    elif keyword == 'STUB_SAVE_DIALOG':
                        # 允许写在【元素】或【测试数据】里，先取原始串
                        raw_path = (kv.get('filePath') or kv.get('path') or data_text or element_text).strip()

                        # ★ 关键：把“单字符串”包进字典，复用 replace_dict 的替换/求值链路
                        _tmp = {'filePath': raw_path}
                        replace_dict(_tmp)
                        file_path = _tmp['filePath']  # 这里的 <name> / <py表达式> 都已被展开

                        if not file_path:
                            raise RuntimeError("STUB_SAVE_DIALOG 需要提供 filePath（如 C:\\Saved_files\\xxx.mop）")

                        logger.info(f"# [STUB_SAVE_DIALOG] filePath={file_path}")
                        g.driver.stub_save_dialog(file_path)
                        step['result'] = 'success'
                        continue


                    # elif keyword == 'STUB_SAVE_DIALOG':
                    #     file_path = (kv.get('filePath') or kv.get('path') or data_text or element_text).strip()
                    #     file_path = replace(file_path)  # ★ 关键：在这里展开 <name> 等变量
                    #     if not file_path:
                    #         raise RuntimeError("STUB_SAVE_DIALOG 需要提供 filePath（如 C:\\work\\cases\\demo.mop）")
                    #
                    #     logger.info(f"# [STUB_SAVE_DIALOG] filePath={file_path}")
                    #     g.driver.stub_save_dialog(file_path)
                    #     step['result'] = 'success'
                    #     continue

                    elif keyword == 'STUB_MESSAGE_BOX':
                        # 支持在【测试数据】写 choice=Yes / OK / No ...，默认 OK
                        choice = (kv.get('choice') or kv.get('button') or 'OK').strip()
                        logger.info(f"# [STUB_MESSAGE_BOX] choice={choice}")
                        g.driver.stub_message_box(choice=choice, once=True)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'RESTORE_DIALOG':
                        g.driver.restore_dialog()
                        step['result'] = 'success'
                        continue

                    elif keyword == 'APP_CHAIN_RELAUNCH':
                        exe_path = kv.get('path') or kv.get('executablePath') or ''
                        process_name = kv.get('processName') or kv.get('proc') or ''
                        logger.info(f"# [APP_CHAIN_RELAUNCH] exe={exe_path or '(unknown)'} proc={process_name} wait={wait_ms}ms")
                        g.driver.chain_relaunch(exe_path=exe_path, process_name=process_name, wait_ms=wait_ms)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'SWITCH_WINDOW':
                        idx = int(kv.get('index') or 0)
                        logger.info(f"# [SWITCH_WINDOW] index={idx}")
                        g.driver.switch_window(idx)
                        step['result'] = 'success'
                        continue

                    elif keyword in ('USE_APP', 'APP_SWITCH'):
                        name = kv.get('name') or element_text or 'app'
                        logger.info(f"# [USE_APP] name={name}")
                        g.driver.set_active(name)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'EXIT':
                        logger.info("# [EXIT] 退出 node worker / 受控 Electron")
                        try:
                            g.driver.exit()
                        except Exception as e:
                            # g.log.warning(f"[EXIT] 忽略退出异常：{e}")
                            logger.warning(f"[EXIT] 忽略退出异常：{e}")
                        step['result'] = 'success'
                        continue

                    elif keyword == 'KILL_PROCESS':
                        # 支持在【测试数据】或【元素】写：proc=MaxOpticsStudio.exe
                        proc = step['data']['proc_name']  or step['data']['name'] or ''
                        proc = proc.replace('proc=', '').replace('process=', '').replace('name=', '').strip()
                        if not proc:
                            raise RuntimeError("KILL_PROCESS 需要进程名，例如 proc=MaxOpticsStudio.exe")
                        logger.info(f"# [KILL_PROCESS] proc={proc}")
                        g.driver.kill_process(proc)
                        step['result'] = 'success'
                        continue

                    elif keyword == 'CLEANUP':
                        # 直接在 Python 侧调用你现有的清理方法
                        logger.info("# [CLEANUP] te_11.kill_max_processes() + te_11.release_max_ports()")
                        try:
                            import te_11
                            try:
                                te_11.kill_max_processes()
                            except Exception as e:
                                g.log.warning(f"kill_max_processes error: {e}")
                            try:
                                te_11.release_max_ports()
                            except Exception as e:
                                g.log.warning(f"release_max_ports error: {e}")
                        except Exception as e:
                            g.log.warning(f"te_11 module not available: {e}")
                        step['result'] = 'success'
                        continue

                    elif keyword == 'EXIT_OR_KILL':  # 可选增强：先优雅退出，超时后强退
                        timeout_ms = int(kv.get('timeout') or 3000)
                        proc = kv.get('proc') or kv.get('process') or kv.get('name') or ''
                        logger.info(f"# [EXIT_OR_KILL] try EXIT, then kill if needed. proc={proc} wait={timeout_ms}ms")
                        try:
                            g.driver.exit()
                        except Exception as e:
                            g.log.warning(f"[EXIT_OR_KILL] EXIT error: {e}")

                        # 等待一会儿看进程是否退出（简化版：直接 sleep）
                        import time
                        time.sleep(timeout_ms / 1000)

                        # 如果提供了进程名，就强退兜底
                        if proc:
                            try:
                                g.driver.kill_process(proc)
                            except Exception as e:
                                g.log.warning(f"[EXIT_OR_KILL] kill_process error: {e}")
                        step['result'] = 'success'
                        continue

                    else:
                        raise RuntimeError(f"未支持的 Windows/Electron 关键字：{keyword}")

                elif step['keyword'].upper() in common_keywords:
                    keyword = common_keywords[step['keyword'].upper()]  # 归一化，例如 ‘执行’ → 'EXECUTE'
                    if keyword == 'EXECUTE':
                        result, steps = common.execute(step)
                        if step['page'] in ('SNIPPET', '用例片段'):
                            self.snippet_steps[index + 1] = steps
                        if result != 'success':
                            self.testcase['result'] = result
                            step['end_timestamp'] = timestamp()
                            break
                    else:
                        getattr(common, keyword.lower())(step)

                # ----------------- HTTP / FILES / Common（保持原逻辑） -----------------
                elif step['keyword'].upper() in http_keywords:
                    getattr(http, step['keyword'].lower())(step)

                elif step['keyword'].upper() in files_keywords:
                    getattr(files, step['keyword'].lower())(step)

                elif step['keyword'].lower() == 'execute':
                    result, steps = getattr(common, step['keyword'].lower())(step)
                    if step['page'] in ('SNIPPET', '用例片段'):
                        self.snippet_steps[index + 1] = steps
                    if result != 'success':
                        self.testcase['result'] = result
                        step['end_timestamp'] = timestamp()
                        break

                else:
                    getattr(common, step['keyword'].lower())(step)

                logger.info('>>>>>>>>>>>>>>>>>>>> success <<<<<<<<<<<<<<<<<<<<')
                step['score'] = 'OK'

                if step['control'] == '^':
                    if_result = True

                if after_function:
                    replace_dict({'after_function': after_function})

                sleep(0.2)

            except Exception as exception:
                # 失败截图（保持原逻辑）
                if g.platform.lower() in ('desktop',) and step['keyword'].upper() in web_keywords:
                    file_name = label + now() + '#Failure' + '.png'
                    step['snapshot']['Failure'] = str(snap.snapshot_folder / file_name)
                    try:
                        if w.frame != 0:
                            g.driver.switch_to.default_content()
                            w.frame = 0
                        g.driver.get_screenshot_as_file(step['snapshot']['Failure'])
                    except Exception:
                        logger.exception('*** 保存截图失败 ***')

                elif g.platform.lower() in ('ios', 'android') and step['keyword'].upper() in mobile_keywords:
                    file_name = label + now() + '#Failure' + '.png'
                    step['snapshot']['Failure'] = str(snap.snapshot_folder / file_name)
                    try:
                        g.driver.switch_to.context('NATIVE_APP')
                        w.current_context = 'NATIVE_APP'
                        g.driver.get_screenshot_as_file(u'%s' % step['snapshot']['Failure'])
                    except Exception:
                        logger.exception('*** 保存截图失败 ***')

                logger.exception('Exception:')
                logger.error('>>>>>>>>>>>>>>>>>>>> failure <<<<<<<<<<<<<<<<<<<<')

                step['score'] = 'NO'
                step['remark'] += str(exception)
                step['end_timestamp'] = timestamp()

                if step['control'] == '^':
                    if_result = False
                    continue

                self.testcase['result'] = 'failure'
                self.testcase['report'] = 'step-%s|%s|%s: %s' % (
                    step['no'], step['keyword'], step['element'], exception)
                break

            # 步骤结束时间
            step['end_timestamp'] = timestamp()

        # 片段展开（保持原逻辑）
        steps = []
        i = 0
        for k in self.snippet_steps:
            steps += self.testcase['steps'][i:k] + self.snippet_steps[k]
            i = k
        steps += self.testcase['steps'][i:]
        self.testcase['steps'] = steps
